package net.shambolica.ember.node;

import com.ericsson.otp.erlang.*;
import net.shambolica.ember.jinterface.wrappers.objects.EOPattern;
import net.shambolica.ember.jinterface.wrappers.objects.EOT;
import net.shambolica.ember.jinterface.wrappers.process.OtpProcessWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static net.shambolica.ember.jinterface.wrappers.objects.EOP.*;
import static net.shambolica.ember.jinterface.wrappers.objects.EOT.atom;
import static net.shambolica.ember.utils.StringUtils.isEmpty;

/**
 * Logger process for an Ember Node.
 * <br/>Waits for messages of the form {log,Level,Package,Message} or an exit, where:
 * <br/>- Level :: 'trace'|'debug'|'info'|'warn'|'error'
 * <br/>- Package :: atom(). Note that the atom must be composed of the characters [a-zA-Z0-9._].
 * <br/>- Message :: utf8_string() | term()
 * <br/>In case of a term() as Message, its toString() will be displayed (TODO: pretty print).
 * <br/>In case of an exit received, will simply return.
 */
public class EmberLogger {

    public static enum Level implements Comparable<Level> {
        trace(1),
        debug(2),
        info(3),
        warn(4),
        error(5);

        private final OtpErlangAtom atom;
        private final int idx;

        Level(int index) {
            this.atom = new OtpErlangAtom(name());
            this.idx = index;
        }

        public OtpErlangAtom getAtom() {
            return atom;
        }

        public int getIndex() {
            return idx;
        }
        
    }


    private volatile Level currentLevel;

    private final OtpProcessWrapper process;
    private final Logger log;
    private final String logName;

    private final ConcurrentHashMap<String,Logger> loggers;


    public EmberLogger(OtpProcessWrapper process, String name) {
        this(process, null, name);
    }
    

    public EmberLogger(OtpProcessWrapper process, Level initialLevel, String name) {
        this.process = process;
        this.currentLevel = initialLevel == null ? Level.debug : initialLevel;
        this.logName = EmberLogger.class.getName() + (isEmpty(name) ? "" : "."+name);
        this.log = LoggerFactory.getLogger(this.logName);
        loggers = new ConcurrentHashMap<String, Logger>();
        log.info("created ember logger: process={} initialLevel={} name={}", new Object[]{
                this.process, this.currentLevel, this.logName});
        goRun();
    }


    private static final EOPattern logMessagePattern = 
            ptuple(atom("log"),
                    pand(pis_atom(),pvar("Level")),
                    pand(pis_atom(),pvar("Package")),
                    pvar("Message"));

    
    /**
     * Main loop.
     */
    protected void goRun() {
        log.info("running: {}", logName);
        try {
            while (true) {
                OtpErlangObject obj = process.receive();
                log.debug("got {}", obj);
                Map<String,OtpErlangObject> match = logMessagePattern.match(obj);
                if (match == null) {
                    log.warn("invalid log message: {}", obj);
                } else {
                    // Vars:
                    String levelVar = ((OtpErlangAtom)match.get("Level")).atomValue();
                    String packageVar = ((OtpErlangAtom)match.get("Package")).atomValue();
                    OtpErlangObject messageVar = match.get("Message");

                    // Set level and target.
                    Level level = Level.info;
                    if (level.getIndex() >= currentLevel.getIndex()) {
                        Logger target = getLogger(isEmpty(packageVar) ? "default" : packageVar);
                        try {
                            level = Level.valueOf(levelVar);
                        } catch (IllegalArgumentException e) {
                            log.warn("invalid log level ({}) in {}", levelVar, obj);
                        }

                        // Construct message.
                        String message;
                        if (messageVar instanceof OtpErlangList) {
                            try {
                                message = ((OtpErlangList) messageVar).stringValue();
                            } catch (OtpErlangException e) {
                                message = messageVar.toString();
                            }
                        } else if (messageVar instanceof OtpErlangAtom) {
                            message = ((OtpErlangAtom) messageVar).atomValue();
                        } else {
                            message = messageVar.toString();
                        }

                        switch (level.getIndex()) {
                            case 1:
                                target.trace(message);
                                break;
                            case 2:
                                target.debug(message);
                                break;
                            case 3:
                                target.info(message);
                                break;
                            case 4:
                                target.warn(message);
                                break;
                            case 5:
                                target.error(message);
                                break;
                            default:
                                target.warn("ember logger bad level {}. message is: {}", level.getIndex(), message);
                                break;
                        }
                    }
                }
            }
        } catch (OtpErlangDecodeException e) {
            log.info("decode exception while receiving: {}", e.getMessage(), e);
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        } catch (OtpErlangExit e) {
            log.info("exit received by logger {}: {}", e.getMessage(), e.reason());
        } finally {
            log.info("exiting logger {}", logName);
        }
    }


    /**
     * Shutdown, as would be called by IoC containter.
     */
    public void shutdown() {
        process.sendExit(new OtpErlangAtom("shutdown"));
    }


    /**
     * Retrieve logger for package. This will create a new logger with correct path name
     * and place it in <code>loggers</code>. Beware namespace inflation!
     * Note: might create superfluous logger temporarily, should be no prob in long run.
     * @param logPackage    Package name.
     * @return              A logger for the package if package well formed, or null.
     */
    protected Logger getLogger(String logPackage) {
        if (isEmpty(logPackage)) {
            return null;
        }

        Logger ret = loggers.get(logPackage);

        if (ret == null) {
            // Check that logPackage is well formed.
            for (byte it : logPackage.getBytes()) {
                if (!(it >= 'a' && it <= 'z' ||
                        it >= 'A' && it <= 'Z' ||
                        it >= '0' && it <= '9' ||
                        it == '.' || it == '_')) {
                    return null;
                }
            }
            ret = LoggerFactory.getLogger(this.logName + "." + logPackage);
            loggers.put(logPackage, ret);
        }

        return ret;

    }

}
